//******************************************************************
//******************************************************************
//**********          ANts Peer To Peer Sources        *************
//
// ANts P2P realizes a third generation P2P net. It protects your
// privacy while you are connected and makes you not trackable, hiding
// your identity (ip) and crypting everything you are sending/receiving
// from others.

// Copyright (C) 2004  Roberto Rossi

// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

package ants.p2p.security;

import java.math.BigInteger;
import java.security.*;
import java.security.spec.*;
import java.security.interfaces.*;
import javax.crypto.*;
import javax.crypto.spec.*;
import javax.crypto.interfaces.*;
import com.sun.crypto.provider.SunJCE;
import java.util.*;

import ants.p2p.*;
import ants.p2p.filesharing.*;
import ants.p2p.messages.*;

import org.apache.log4j.*;

public class EndpointSecurityManager extends Thread implements Comparable{
  public static final String cipher = "AES";
  public static final int cipherKeySize = 16;
  public static final int DHKeyExchangeBits = 512;
  public static final int MinDHKeyExchangeBits = 512;
  public static final long inactivityTimeout = 60 * 1000 * 30;
  public static final long refreshInterval = 60 * 1000;

  static Logger _logger = Logger.getLogger(EndpointSecurityManager.class.getName());

  DHParameterSpec dhParamSpec;
  KeyPair requirerKpair;
  KeyAgreement requirerKeyAgree;
  KeyPair peerKpair;
  KeyAgreement peerKeyAgree;
  PublicKey requirerPubKey;
  PublicKey peerPubKey;
  Message securityMessage;
  long lastTimeUsed = System.currentTimeMillis();

  boolean isRequirer;

  private WarriorAnt n;
  private String peerId;
  private String requirerId;

  private byte[] sharedSecret;
  private byte[] aesKey;
  private Cipher requirerCipherEnc;
  private Cipher requirerCipherDec;
  private Cipher peerCipherEnc;
  private Cipher peerCipherDec;

  public EndpointSecurityManager(WarriorAnt n, String peerId) throws Exception{
    this.isRequirer = true;
    this.requirerId = n.getIdent();
    this.peerId = peerId;
    this.n = n;
    this.sendSecurityRequest();
    this.start();
  }

  public EndpointSecurityManager(WarriorAnt n, SecurityRequestMessage srm) throws Exception{
    this.isRequirer=false;
    this.requirerId=srm.getSource();
    this.peerId=n.getIdent();
    this.n=n;
    this.securityMessage=new Message(srm);
    this.processSecurityRequestMessage(srm);
    this.start();
  }

  public int compareTo(Object o){
    if(o instanceof EndpointSecurityManager){
      long compare = this.getLastTimeUsed()-((EndpointSecurityManager)o).getLastTimeUsed();
      if(compare > 0)
        return -1;
      else if(compare < 0)
        return 1;
      else
        return 0;
    }else
      return 0;
  }

  public long getLastTimeUsed(){
    return this.lastTimeUsed;
  }

  public void resetLastTimeUsed(){
    this.lastTimeUsed = 0;
  }

  public void setLastTimeUsed(){
    this.lastTimeUsed = System.currentTimeMillis();
  }

  public void run() {
    try {
      while (!n.isDisconnected() && System.currentTimeMillis() - this.getLastTimeUsed() <
             inactivityTimeout) {
        this.sleep(refreshInterval);
        _logger.info("Secure connection with " + this.getPeerId().substring(0,10) + " has " +
                     (System.currentTimeMillis() - this.getLastTimeUsed()) /
                     1000 + " inactivity seconds (timout at " +
                     inactivityTimeout / 1000 + ")");
        if(this.isRequirer){
          if(!this.n.outputSecureConnections.contains(this))
            return;
        }else{
          if(!this.n.inputSecureConnections.contains(this))
            return;
        }
      }
    }
    catch (InterruptedException ex) {
    }
    if (this.isRequirer()) {
      this.n.removeOutputSecureConnection(this.getPeerId());
      n.propertyChangeSupport.firePropertyChange(
          "removedOutputSecureConnection", null, this.getPeerId());
    }
    else {
      this.n.removeInputSecureConnection(this.getPeerId());
    }
  }

  public String getPeerId(){
    if(this.isRequirer()){
      return this.peerId;
    }else{
      return this.requirerId;
    }
  }

  public boolean isRequirer(){
    return this.isRequirer;
  }

  private void generateDHParameters() throws
      NoSuchAlgorithmException,
      InvalidParameterSpecException,
      InvalidAlgorithmParameterException,
      InvalidKeyException {
    if (this.isRequirer()) {
      AlgorithmParameterGenerator paramGen = AlgorithmParameterGenerator.
          getInstance("DH");
      paramGen.init(EndpointSecurityManager.DHKeyExchangeBits);
      AlgorithmParameters params = paramGen.generateParameters();
      dhParamSpec = (DHParameterSpec) params.getParameterSpec(DHParameterSpec.class);

      KeyPairGenerator requirerKpairGen = KeyPairGenerator.getInstance("DH");
      requirerKpairGen.initialize(dhParamSpec);
      requirerKpair = requirerKpairGen.generateKeyPair();

      requirerKeyAgree = KeyAgreement.getInstance("DH");
      requirerKeyAgree.init(requirerKpair.getPrivate());

      requirerPubKey = requirerKpair.getPublic();

    }
    else {
      dhParamSpec = new DHParameterSpec(skip1024Modulus, skip1024Base);

      KeyPairGenerator peerKpairGen = KeyPairGenerator.getInstance("DH");
      peerKpairGen.initialize(dhParamSpec);
      peerKpair = peerKpairGen.generateKeyPair();

      peerKeyAgree = KeyAgreement.getInstance("DH");
      peerKeyAgree.init(peerKpair.getPrivate());

      peerPubKey = peerKpair.getPublic();

    }
  }

  public void sendSecurityRequest() throws Exception{
    if(!this.isRequirer)
      throw new Exception("Violation of security protocol");
    try {
      this.generateDHParameters();
      this.requirerCipherEnc = Cipher.getInstance(EndpointSecurityManager.cipher);
      this.requirerCipherDec = Cipher.getInstance(EndpointSecurityManager.cipher);

      SecurityRequestMessage srm = new SecurityRequestMessage(this.dhParamSpec.
          getG(), this.dhParamSpec.getP(), requirerPubKey);
      MessageWrapper wm = n.sendMessage(srm, this.peerId, false, false);
      this.securityMessage = new Message(wm);

      n.pendingSecureRequest.add(this);
      Object[] collection = n.pendingSecureRequest.toArray();
      Arrays.sort(collection);
      n.pendingSecureRequest = new ArrayList(Arrays.asList(collection));
    }
    catch (Exception e) {
      _logger.error("",e);
      this.lastTimeUsed = 0;
    }
  }

  public Message getSecurityMessage(){
    return this.securityMessage;
  }

  public void processSecurityResponseMessage(SecurityResponseMessage srm) throws Exception {
    if (!this.isRequirer)
      throw new Exception("Violation of security protocol");

    try {
      this.peerPubKey = srm.getPeerPubkey();
      if(this.peerPubKey.getEncoded().length*8 < EndpointSecurityManager.MinDHKeyExchangeBits){
        throw new InvalidAlgorithmParameterException(
            "Endpoint security Error: key size < "+EndpointSecurityManager.MinDHKeyExchangeBits);
      }

      KeyFactory serverKeyFac = KeyFactory.getInstance("DH");
      requirerKeyAgree.doPhase(peerPubKey, true);

      this.sharedSecret = requirerKeyAgree.generateSecret();

      this.aesKey = new byte[EndpointSecurityManager.cipherKeySize];
      for (int x = 0; x < this.aesKey.length; x++) {
        this.aesKey[x] = this.sharedSecret[x];
      }
      SecretKeySpec serverKeySpec = new SecretKeySpec(aesKey, EndpointSecurityManager.cipher);
      this.requirerCipherEnc.init(Cipher.ENCRYPT_MODE, serverKeySpec);
      this.requirerCipherDec.init(Cipher.DECRYPT_MODE, serverKeySpec);

      n.pendingSecureRequest.remove(this);
      n.myMessages.remove(this.getSecurityMessage());
      n.outputSecureConnections.add(this);
    }
    catch (Exception e) {
      _logger.error("",e);
      this.lastTimeUsed = 0;
    }
  }

  public Cipher getCipherEnc() {
    this.setLastTimeUsed();
    if(this.isRequirer())
      return this.requirerCipherEnc;
    else{
      return this.peerCipherEnc;
    }
  }

  public Cipher getCipherDec() {
    this.setLastTimeUsed();
    if(this.isRequirer())
      return this.requirerCipherDec;
    else{
      return this.peerCipherDec;
    }
  }


  public void processSecurityRequestMessage(SecurityRequestMessage srm) throws Exception{
    if (this.isRequirer)
        throw new Exception("Violation of security protocol");
      boolean found=false;
      for (int x = n.inputSecureConnections.size() - 1; x >= 0; x--) {
        EndpointSecurityManager esm = (EndpointSecurityManager) n.
            inputSecureConnections.get(x);
        if (esm.getPeerId().equals(this.getPeerId()) && esm.getSecurityMessage().equals(srm)) {
          _logger.debug("Keep current " + esm.getPeerId() + " " +
                       esm.getSecurityMessage().getAck_Id());
          _logger.debug(peerId + " " + srm.getAck_Id());
          found = true;
        }else if (esm.getPeerId().equals(this.getPeerId()) && !esm.getSecurityMessage().equals(srm)) {
          _logger.debug("Remove old " + esm.getPeerId() + " " +
                       esm.getSecurityMessage().getAck_Id());
          _logger.debug(peerId + " " + srm.getAck_Id());
          n.inputSecureConnections.remove(x);
        }
        if(found)
          return;
      }

      this.peerCipherEnc = Cipher.getInstance(EndpointSecurityManager.cipher);
      this.peerCipherDec = Cipher.getInstance(EndpointSecurityManager.cipher);

      this.skip1024Base = srm.getP();
      this.skip1024Modulus = srm.getG();
      this.generateDHParameters();
      this.requirerPubKey = srm.getRequirerPubkey();

      SecurityResponseMessage answer = new SecurityResponseMessage(this.peerPubKey, new Message(srm));
      MessageWrapper wm = n.sendMessage(answer,this.requirerId, false, false);

      KeyFactory peerKeyFac = KeyFactory.getInstance("DH");
      peerKeyAgree.doPhase(this.requirerPubKey, true);

      this.sharedSecret = peerKeyAgree.generateSecret();

      this.aesKey = new byte[EndpointSecurityManager.cipherKeySize];//AES 16
      for (int x = 0; x < this.aesKey.length; x++) {
        this.aesKey[x] = this.sharedSecret[x];
      }
      SecretKeySpec clientKeySpec = new SecretKeySpec(aesKey, EndpointSecurityManager.cipher);
      this.peerCipherEnc.init(Cipher.ENCRYPT_MODE, clientKeySpec);
      this.peerCipherDec.init(Cipher.DECRYPT_MODE, clientKeySpec);

      n.inputSecureConnections.add(this);
      Object[] collection = n.inputSecureConnections.toArray();
      Arrays.sort(collection);
      n.inputSecureConnections = new ArrayList(Arrays.asList(collection));
  }


  // The 1024 bit Diffie-Hellman modulus values used by SKIP
  private static final byte skip1024ModulusBytes[] = {
      (byte) 0xF4, (byte) 0x88, (byte) 0xFD, (byte) 0x58,
      (byte) 0x4E, (byte) 0x49, (byte) 0xDB, (byte) 0xCD,
      (byte) 0x20, (byte) 0xB4, (byte) 0x9D, (byte) 0xE4,
      (byte) 0x91, (byte) 0x07, (byte) 0x36, (byte) 0x6B,
      (byte) 0x33, (byte) 0x6C, (byte) 0x38, (byte) 0x0D,
      (byte) 0x45, (byte) 0x1D, (byte) 0x0F, (byte) 0x7C,
      (byte) 0x88, (byte) 0xB3, (byte) 0x1C, (byte) 0x7C,
      (byte) 0x5B, (byte) 0x2D, (byte) 0x8E, (byte) 0xF6,
      (byte) 0xF3, (byte) 0xC9, (byte) 0x23, (byte) 0xC0,
      (byte) 0x43, (byte) 0xF0, (byte) 0xA5, (byte) 0x5B,
      (byte) 0x18, (byte) 0x8D, (byte) 0x8E, (byte) 0xBB,
      (byte) 0x55, (byte) 0x8C, (byte) 0xB8, (byte) 0x5D,
      (byte) 0x38, (byte) 0xD3, (byte) 0x34, (byte) 0xFD,
      (byte) 0x7C, (byte) 0x17, (byte) 0x57, (byte) 0x43,
      (byte) 0xA3, (byte) 0x1D, (byte) 0x18, (byte) 0x6C,
      (byte) 0xDE, (byte) 0x33, (byte) 0x21, (byte) 0x2C,
      (byte) 0xB5, (byte) 0x2A, (byte) 0xFF, (byte) 0x3C,
      (byte) 0xE1, (byte) 0xB1, (byte) 0x29, (byte) 0x40,
      (byte) 0x18, (byte) 0x11, (byte) 0x8D, (byte) 0x7C,
      (byte) 0x84, (byte) 0xA7, (byte) 0x0A, (byte) 0x72,
      (byte) 0xD6, (byte) 0x86, (byte) 0xC4, (byte) 0x03,
      (byte) 0x19, (byte) 0xC8, (byte) 0x07, (byte) 0x29,
      (byte) 0x7A, (byte) 0xCA, (byte) 0x95, (byte) 0x0C,
      (byte) 0xD9, (byte) 0x96, (byte) 0x9F, (byte) 0xAB,
      (byte) 0xD0, (byte) 0x0A, (byte) 0x50, (byte) 0x9B,
      (byte) 0x02, (byte) 0x46, (byte) 0xD3, (byte) 0x08,
      (byte) 0x3D, (byte) 0x66, (byte) 0xA4, (byte) 0x5D,
      (byte) 0x41, (byte) 0x9F, (byte) 0x9C, (byte) 0x7C,
      (byte) 0xBD, (byte) 0x89, (byte) 0x4B, (byte) 0x22,
      (byte) 0x19, (byte) 0x26, (byte) 0xBA, (byte) 0xAB,
      (byte) 0xA2, (byte) 0x5E, (byte) 0xC3, (byte) 0x55,
      (byte) 0xE9, (byte) 0x2F, (byte) 0x78, (byte) 0xC7
  };
/*
  private static byte[] iv = new byte[]{
                (byte)0x8E, 0x12, 0x39, (byte)0x9C,
                0x07, 0x72, 0x6F, 0x5A
            };
*/
// The SKIP 1024 bit modulus
  private BigInteger skip1024Modulus
      = new BigInteger(1, skip1024ModulusBytes);

// The base used with the SKIP 1024 bit modulus
  private BigInteger skip1024Base = BigInteger.valueOf(2);
}
